package nf.pluginsys;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import nf.SettingsStore;
import nf.modular.ModuleHost;
import nf.modular.NFModule;
import nf.modular.NFModule.NFModuleSignature;
import nf.modular.NFModuleType;
import nf.modular.modclass.Classificator;
import nf.modular.modclass.ImageAnalyzer;
import nf.modular.modclass.ImageLoader;
import nf.modular.modclass.ImagePreprocessor;
import nf.modular.modclass.ResultExporter;
import nf.modular.modclass.Trainer;

public class PluginHost implements ModuleHost
{
	/**
	 * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
	 *
	 * @param packageName The base package
	 * @return The classes
	 * @throws ClassNotFoundException
	 * @throws IOException
	 */
	@SuppressWarnings("rawtypes")
	private static Class[] getClasses(String packageName, ClassLoader classLoader)
	        throws ClassNotFoundException, IOException {
	    assert classLoader != null;
	    String path = packageName.replace('.', '/');
	    Enumeration<URL> resources = classLoader.getResources(path);
	    List<File> dirs = new ArrayList<File>();
	    while (resources.hasMoreElements()) {
	        URL resource = resources.nextElement();
	        dirs.add(new File(resource.getFile()));
	    }
	    ArrayList<Class> classes = new ArrayList<Class>();
	    for (File directory : dirs) {
	        classes.addAll(findClasses(directory, packageName));
	    }
	    return classes.toArray(new Class[classes.size()]);
	}

	/**
	 * Recursive method used to find all classes in a given directory and subdirs.
	 *
	 * @param directory   The base directory
	 * @param packageName The package name for classes found inside the base directory
	 * @return The classes
	 * @throws ClassNotFoundException
	 */
	@SuppressWarnings("rawtypes")
	private static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
	    List<Class> classes = new ArrayList<Class>();
	    if (!directory.exists()) {
	        return classes;
	    }
	    File[] files = directory.listFiles();
	    for (File file : files) {
	        if (file.isDirectory()) {
	            assert !file.getName().contains(".");
	            classes.addAll(findClasses(file, packageName + "." + file.getName()));
	        } else if (file.getName().endsWith(".class")) {
	            classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
	        }
	    }
	    return classes;
	}

	private List<Class<? extends Classificator>> modClassificators = new ArrayList<Class<? extends Classificator>>();
	private List<Class<? extends ImageAnalyzer>> modAnalyzers = new ArrayList<Class<? extends ImageAnalyzer>>();
	private List<Class<? extends ImageLoader>> modLoaders = new ArrayList<Class<? extends ImageLoader>>();
	private List<Class<? extends ImagePreprocessor>> modPreprocessors = new ArrayList<Class<? extends ImagePreprocessor>>();
	private List<Class<? extends ResultExporter>> modExporter = new ArrayList<Class<? extends ResultExporter>>();
	private List<Class<? extends Trainer>> modTrainer = new ArrayList<Class<? extends Trainer>>();
	
	private List<Classificator> instClassificators = new ArrayList<Classificator>();
	private List<ImageAnalyzer> instAnalyzers = new ArrayList<ImageAnalyzer>();
	private List<ImageLoader> instLoaders = new ArrayList<ImageLoader>();
	private List<ImagePreprocessor> instPreprocessors = new ArrayList<ImagePreprocessor>();
	private List<ResultExporter> instExporters = new ArrayList<ResultExporter>();
	private List<Trainer> instTrainers = new ArrayList<Trainer>();
	
	private Map<NFModuleType, List<NFModule>> instanceMap = new HashMap<NFModuleType, List<NFModule>>();
	
	@SuppressWarnings("rawtypes")
	private NFModule instantiate(Class c, Class type)
	{
		//Classificator instance
		try
		{
			@SuppressWarnings("unchecked")
			Constructor<? extends Classificator> cons =  c.getConstructor(ModuleHost.class, String.class);
			Object instance = cons.newInstance(this, SettingsStore.tempFolder.getAbsolutePath());
			System.out.println(instance.toString());
			if(instanceMap.get(((NFModule)instance).getModuleType()) == null)
				instanceMap.put(((NFModule)instance).getModuleType(), new ArrayList<NFModule>());
			instanceMap.get(((NFModule)instance).getModuleType()).add((NFModule)instance);
			return (NFModule)instance;
		} 
		catch (NoSuchMethodException e)
		{
			e.printStackTrace();
			return null;
		} 
		catch (SecurityException e)
		{
			e.printStackTrace();
			return null;
		} catch (InstantiationException e)
		{
			e.printStackTrace();
			return null;
		} catch (IllegalAccessException e)
		{
			e.printStackTrace();
			return null;
		} catch (IllegalArgumentException e)
		{
			e.printStackTrace();
			return null;
		} catch (InvocationTargetException e)
		{
			e.printStackTrace();
			return null;
		}
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private boolean categorizeClass(Class c)
	{
		if(Classificator.class.isAssignableFrom(c))
		{
			System.out.println("Found classificator : " + c.getName());
			modClassificators.add(c);
			Classificator instance = (Classificator) instantiate(c, Classificator.class);
			if(instance != null)
			{
				instClassificators.add(instance);
				return true;
			}
			else return false;
		}
		else if(ImageAnalyzer.class.isAssignableFrom(c))
		{
			System.out.println("Found analyzer : " + c.getName());
			modAnalyzers.add(c);
			ImageAnalyzer instance = (ImageAnalyzer) instantiate(c, Classificator.class);
			if(instance != null)
			{
				instAnalyzers.add(instance);
				return true;
			}
			else return false;
		}
		else if(ImageLoader.class.isAssignableFrom(c))
		{
			System.out.println("Found loader : " + c.getName());
			modLoaders.add(c);
			ImageLoader instance = (ImageLoader) instantiate(c, Classificator.class);
			if(instance != null)
			{
				instLoaders.add(instance);
				return true;
			}
			else return false;
		}
		else if(ImagePreprocessor.class.isAssignableFrom(c))
		{
			System.out.println("Found preprocessor : " + c.getName());
			modPreprocessors.add(c);
			ImagePreprocessor instance = (ImagePreprocessor) instantiate(c, Classificator.class);
			if(instance != null)
			{
				instPreprocessors.add(instance);
				return true;
			}
			else return false;
		}
		else if(ResultExporter.class.isAssignableFrom(c))
		{
			System.out.println("Found exporter : " + c.getName());
			modExporter.add(c);
			ResultExporter instance = (ResultExporter) instantiate(c, Classificator.class);
			if(instance != null)
			{
				instExporters.add(instance);
				return true;
			}
			else return false;
		}
		else if(Trainer.class.isAssignableFrom(c))
		{
			System.out.println("Found trainer : " + c.getName());
			modTrainer.add(c);
			Trainer instance = (Trainer) instantiate(c, Trainer.class);
			if(instance != null)
			{
				instTrainers.add(instance);
				return true;
			}
			else return false;
		}
		else
		{
			return false;
		}
	}
	
	@SuppressWarnings("rawtypes")
	public boolean reindexModules()
	{
		System.out.println("INDEXING BEGIN");
		
		modClassificators.clear();
		modAnalyzers.clear();
		modLoaders.clear();
		modPreprocessors.clear();
		modExporter.clear();
		modTrainer.clear();
		
		instClassificators.clear();
		instAnalyzers.clear();
		instLoaders.clear();
		instPreprocessors.clear();
		instExporters.clear();
		instTrainers.clear();
		
		try
		{
			Class[] classes = getClasses("nf.plugins", Thread.currentThread().getContextClassLoader());
			for(Class c : classes)
			{
				//System.out.println(c.toString());
				categorizeClass(c);
			}
		} 
		catch (ClassNotFoundException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		System.out.println(" - ");
		System.out.println("Total loaders : " + modLoaders.size());
		System.out.println("Total preprocessors : " + modPreprocessors.size());
		System.out.println("Total analyzers : " + modAnalyzers.size());
		System.out.println("Total classificators : " + modClassificators.size());
		System.out.println("Total exporters : " + modExporter.size());
		System.out.println("Total trainers : " + modTrainer.size());
		
		System.out.println("INDEXING END");
		return true;
	}
	
	@Override
	public NFModule queryModule(NFModuleSignature signature)
	{
		List<NFModule> category = instanceMap.get(signature.type);
		if(category == null)
			return null;
		if(category.size() == 0)
			return null;
		if(signature.name != null)
		{
			String lowerName = signature.name.toLowerCase();
			for(NFModule mod : category)
			{
				if(mod.getModuleName().toLowerCase().equals(lowerName))
				{
					if(signature.versionInt > 0)
					{
						if(signature.versionInt <= mod.getVersionInt())
						{
							return mod;
						}
					}
					else
					{
						return mod;
					}
				}
			}
			return null;
		}
		else
		{
			return category.get(0);
		}
	}
	
	/*
	private static void findClassesInZip(List<String> list, ZipEntry entry)
	{
		for(ZipEntry e : entry.get)
	}
	*/
	
	public boolean loadAllPlugins(File dir)
	{
		if(dir.isFile())
		{
			loadPlugin(dir);
		}
		else if(dir.isDirectory())
		{
			File[] files = dir.listFiles();
			for(File f : files)
				loadAllPlugins(f);
		}
		return true;
	}
	
	@SuppressWarnings("rawtypes")
	public boolean loadPlugin(File modFile)
	{
		if(modFile == null) return false;
		if(!modFile.exists()) return false;
		if(!modFile.isFile()) return false;
		
		if(!modFile.getName().endsWith(".jar")) return false;
		
		try
		{
			
			List<String> list = new ArrayList<String>();
		    URL jar = modFile.toURI().toURL();
		    ZipInputStream zip = new ZipInputStream( jar.openStream());
		    ZipEntry ze = null;

		    while( ( ze = zip.getNextEntry() ) != null ) {
		        String entryName = ze.getName();
		        if( entryName.endsWith(".class") ) {
		        	entryName = entryName.replace('/', '.');
		        	entryName = entryName.substring(0, entryName.length() - 6);
		            list.add( entryName  );
		            System.out.println("CLASS:" + entryName);
		        }
		    }
			
			URL url = modFile.toURI().toURL();  
			URL[] urls = new URL[]{url};
			URLClassLoader cl = new URLClassLoader(urls);
			
			System.out.println(url.toString());
			System.out.println(modFile.exists());
			
			//int loaded = 0;
			int plugins = 0;
			
			for(String entry : list)
			{
				//Class loadedClass = cl.loadClass("nf.plugins.exporter.htmlex.HTMLExporter");
				Class loadedClass = cl.loadClass(entry);
				
				//Class loadedClass = cl.loadClass("HTMLExporter");
				if(loadedClass != null)
				{
					//loaded++;
					System.out.println("Yay, loaded " + loadedClass.getName());
					if(categorizeClass(loadedClass))
					{
						System.out.println("It's a NFModule!");
						plugins++;
					}
				}
				else
				{
					System.out.println("FAIL :(");
				}
			}
			
			cl.close();
			
			/*
			Class[] classes = getClasses("nf.plugins", cl);
			System.out.println("NEW STUFF BEGIN");
			for(Class c : classes)
			{
				System.out.println(c.toString());
			}
			System.out.println("NEW STUFF END");
			*/
			return plugins > 0;
		} 
		catch (MalformedURLException e)
		{
			e.printStackTrace();
			return false;
		} catch (ClassNotFoundException e)
		{
			e.printStackTrace();
			return false;
		} catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
	}
	
	public List<Classificator> getClassificators()
	{
		return instClassificators;
	}

	public List<ImageAnalyzer> getAnalyzers()
	{
		return instAnalyzers;
	}

	public List<ImageLoader> getLoaders()
	{
		return instLoaders;
	}

	public List<ImagePreprocessor> getPreprocessors()
	{
		return instPreprocessors;
	}

	public List<ResultExporter> getExporters()
	{
		return instExporters;
	}
	
	public List<Trainer> getTrainers()
	{
		return instTrainers;
	}

}
